feat(server): lossless speculative tool execution — stream early dispatch, dependency waves, end-of-turn KV, prefetch-prefill (x1.44 on live-API agent loops) - #614
Conversation
There was a problem hiding this comment.
38 issues found across 38 files
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="server/src/server/tool_speculation.cpp">
<violation number="1" location="server/src/server/tool_speculation.cpp:127">
P2: The child receives the entire server environment, including unrelated credentials and model configuration. Construct a minimal allowlisted environment instead of forwarding `environ`.</violation>
<violation number="2" location="server/src/server/tool_speculation.cpp:779">
P2: The FD-isolation guarantee is only enforced on glibc >= 2.34. `addclosefrom_np` is guarded by `__GLIBC_PREREQ(2, 34)`, so on glibc 2.17-2.33, musl-based Linux, or any non-glibc POSIX build the child executor inherits every open descriptor from the long-running server (listening sockets, live client connections, model IPC pipes, accelerator descriptors) — directly contradicting the header comment "The executor receives only stdin/stdout/stderr" and the PR's privacy claim. The failure is silent: the code compiles and launches without any FD isolation.</violation>
<violation number="3" location="server/src/server/tool_speculation.cpp:802">
P2: When CPU affinity is configured, `posix_spawn` lets the executor run before `pin_and_verify_child_cpu_affinity` applies the mask. Startup work can consume model CPUs before isolation; spawn through a pre-exec affinity handshake instead.</violation>
</file>
<file name="optimizations/ooo_spec_lucebox5_cpu/benchmark_cpu_tool_speculation.py">
<violation number="1" location="optimizations/ooo_spec_lucebox5_cpu/benchmark_cpu_tool_speculation.py:334">
P1: When model generation consumes the configured timeout, `finish_executor` grants the tool another full timeout because `communicate` starts timing too late. Compute the remaining timeout from `handle["started"]` before waiting.</violation>
<violation number="2" location="optimizations/ooo_spec_lucebox5_cpu/benchmark_cpu_tool_speculation.py:336">
P2: Timeout and miss cancellation kill only the executor leader, so forked descendants can survive and retain the CPU lane. Launch the executor in its own process group and terminate the group on every cleanup path.</violation>
<violation number="3" location="optimizations/ooo_spec_lucebox5_cpu/benchmark_cpu_tool_speculation.py:504">
P2: If `post_model` raises after the private miss executor starts, `run_direct_miss` skips cleanup and the process continues using reserved CPUs. Put the model request and cleanup in a `try/finally`.</violation>
<violation number="4" location="optimizations/ooo_spec_lucebox5_cpu/benchmark_cpu_tool_speculation.py:565">
P2: The private_miss_result_hidden qualification gate is vacuous: run_direct_miss hardcodes 'private_result_exposed': False rather than deriving it, and the private executor is launched by the benchmark itself, so it is never routed through the engine and cannot expose a result. The check in qualify() therefore always passes and adds no real privacy coverage. The native() phase's miss_check ('result' in miss_metadata) is the actual isolation test. Either drop the qualify() gate or compute it from the native path; don't report it as a passed safety gate.</violation>
<violation number="5" location="optimizations/ooo_spec_lucebox5_cpu/benchmark_cpu_tool_speculation.py:1157">
P2: The automatic-Qwen production gate can run without explicitly verifying a qualified profile or disjoint model CPUs. Require the same profile and model/tool affinity checks as the `native` gate before measuring pairs.</violation>
</file>
<file name="server/src/common/qwen3_tool_predictor_ipc.h">
<violation number="1" location="server/src/common/qwen3_tool_predictor_ipc.h:29">
P2: When the configured IPC binary never sends its initial status, `start()` blocks in `BackendIpcProcess::start()` and server startup never reaches `run()`. Add a bounded readiness deadline, terminate the child on expiry, and treat the native predictor as unavailable so the configured HTTP fallback or normal server path can proceed.</violation>
</file>
<file name="server/src/server/native_semantic_tool_predictor.cpp">
<violation number="1" location="server/src/server/native_semantic_tool_predictor.cpp:52">
P2: When prompt construction or tokenization exceeds `config_.timeout_ms`, `predict()` still blocks before its only deadline check, so before-model requests exceed the configured timeout and HTTP fallback loses its remaining budget. Make preprocessing deadline-aware or otherwise bound it before entering the predictor lane.</violation>
</file>
<file name="server/src/server/server_main.cpp">
<violation number="1" location="server/src/server/server_main.cpp:626">
P2: When `--tool-hint-native-gpu` is malformed, `std::atoi` silently selects GPU 0. Parse this option strictly and reject non-numeric or out-of-range values before starting the native predictor.</violation>
<violation number="2" location="server/src/server/server_main.cpp:673">
P2: When `--tool-hint-execution-confidence` is malformed, `std::atof` converts it to `0` and startup accepts it. Parse the argument strictly and reject trailing or non-numeric input instead of silently changing the admission policy.</violation>
</file>
<file name="server/src/common/qwen3_tool_predictor_ipc_daemon.cpp">
<violation number="1" location="server/src/common/qwen3_tool_predictor_ipc_daemon.cpp:90">
P2: For predictions of at least three tokens, this lane produces later logits from a skipped Qwen3 KV-cache position. Fix `Qwen3Backend::do_decode`'s position update before routing predictor requests through this backend.</violation>
</file>
<file name="optimizations/ooo_spec_lucebox5_cpu/results/trace-compiled-engine-qwen-production-6pairs-compact.json">
<violation number="1" location="optimizations/ooo_spec_lucebox5_cpu/results/trace-compiled-engine-qwen-production-6pairs-compact.json:653">
P2: The trace-compilation provenance recorded in this results artifact cannot be reproduced from the repo. `pattern.training_report` and `pattern.workflow_registry` point to absolute `/home/lucebox5/tool-spec-cpu-20260813/results/...` paths to files that are not committed (`multiturn-cached-wordref-production-6tasks.json`, `trace-workflow-registry.json`), while the README's reproduce command feeds `results/trace-compiled-training-traces.json`, whose sha256 is `ba73d1aef7c6...`, not the recorded `training_report_sha256` `2475697d...`. The recorded hashes therefore match nothing in the repo, so the 6/6 exact-hit and gate evidence cannot be independently regenerated or verified. Record the canonical source path (relative to the repo), commit the workflow registry that defines the `execute_customer_workflows` macro allowlist/canonicalization, and make the recorded sha256 correspond to the committed input.</violation>
</file>
<file name="server/src/server/http_server.cpp">
<violation number="1" location="server/src/server/http_server.cpp:2467">
P2: `launch_semantic_tool_prediction` shares one `NativeSemanticToolPredictor` instance (`native_semantic_predictor_`) across every request and invokes `native->predict(payload, tools)` from a fresh `std::async` thread per request. `predict` is non-const and the object owns a `Qwen3ToolPredictorIpcClient ipc_` with no visible synchronization; concurrent tool-using requests can therefore call `predict` concurrently on the same IPC client. If that client is not internally thread-safe, this is a data race. Additionally, spawning two `std::async(std::launch::async)` threads per tool request is unbounded under concurrency.</violation>
<violation number="2" location="server/src/server/http_server.cpp:2664">
P2: When upstream proxy mode is enabled with a before-model native predictor, proxied requests wait for an unused prediction before forwarding. Skip semantic prediction for forwarded requests, or explicitly cancel it before the early `forward_upstream()` return.</violation>
<violation number="3" location="server/src/server/http_server.cpp:4572">
P2: In overlap mode (the default for HTTP predictors, and the experimental native overlap schedule), `launch_semantic_tool_prediction` starts the predictor on a `std::async` worker before the request is enqueued, then `finish_tool_speculation` calls `req.automatic_tool_speculation.get()` / `req.semantic_tool_prediction.get()`. If generation finishes before the prediction (e.g. the model returns a plain answer with no tool call, or a short response), `.get()` blocks the worker thread for the remaining predictor time (up to `--tool-hint-timeout-ms`, default 2000 ms). This delays that response and, because the worker thread is shared, stalls other queued jobs. The overlap schedule is supposed to run generation and prediction concurrently, so blocking on the prediction here partially negates it.</violation>
</file>
<file name="optimizations/ooo_spec_lucebox5_cpu/cpu_sparse_tool_executor.cpp">
<violation number="1" location="optimizations/ooo_spec_lucebox5_cpu/cpu_sparse_tool_executor.cpp:141">
P1: When an allowlisted call contains these extra fields, the executor can allocate several GiB despite the small public `iterations` schema. Reject unknown arguments and keep the qualified sparse shape fixed, rather than letting model-supplied JSON select dimensions and worker count.</violation>
<violation number="2" location="optimizations/ooo_spec_lucebox5_cpu/cpu_sparse_tool_executor.cpp:179">
P1: When this executor is configured without a CPU lane, the empty expected mask bypasses isolation and the workload competes with model decoding. Require a non-empty expected affinity and fail closed when it is absent.</violation>
</file>
<file name="optimizations/ooo_spec_lucebox5_cpu/run_native_cpu_server_lucebox5.sh">
<violation number="1" location="optimizations/ooo_spec_lucebox5_cpu/run_native_cpu_server_lucebox5.sh:43">
P2: When `CANDIDATE_BUILD` is overridden for this launcher, the qualified launcher clears that ambient override before the wrapper runs, so the wrapper may execute its symlink/default build instead of the validated build. Pass build selection through a durable launcher-supported mechanism, or preflight the wrapper’s actual candidate.</violation>
<violation number="2" location="optimizations/ooo_spec_lucebox5_cpu/run_native_cpu_server_lucebox5.sh:49">
P2: When `PREDICTOR_TIMEOUT_MS` is overridden, this launcher drops the override before the qualified launcher starts the wrapper, so prediction continues using the 2000 ms default. Propagate the timeout setting here so it reaches `--tool-hint-timeout-ms`.</violation>
</file>
<file name="optimizations/ooo_spec_lucebox5_cpu/bfcl_replay_tool_executor.py">
<violation number="1" location="optimizations/ooo_spec_lucebox5_cpu/bfcl_replay_tool_executor.py:53">
P2: When `cpu_affinity` is explicitly null or another falsy non-list, `or []` skips the affinity check and can report an unisolated executor as successful. Use a default-only lookup so malformed supplied values reach the type validation.</violation>
</file>
<file name="server/src/server/chat_template.cpp">
<violation number="1" location="server/src/server/chat_template.cpp:390">
P2: The new `tool_call_required` parameter is documented (header) as strengthening the template for OpenAI `tool_choice="required"` and forced-function requests, and both call sites in http_server.cpp pass `tool_choice_requires_call(req.tool_choice)`. However it is only honored in the DEEPSEEK4 case (line 390); the QWEN3, LAGUNA, and GEMMA4 branches ignore it entirely. For QWEN3 — the default/primary architecture for the workflows targeted by this PR — a `tool_choice="required"` or forced-function request produces the same prompt as a normal request, and its preamble even says "If there is no function call available, answer the question like normal", so the model is not actually forced to emit a call. Wire the flag into the other tool-capable branches (at minimum QWEN3, and LAGUNA's non-thinking/thinking tool-block) so the required-call contract holds across architectures.</violation>
</file>
<file name="server/src/qwen3/qwen3_loader.cpp">
<violation number="1" location="server/src/qwen3/qwen3_loader.cpp:150">
P1: When the compact GGUF has Q8_0 projection weights but BF16 embeddings or output weights, this assignment applies Q8_0 to every 2-D tensor and the drafter fails to load. Allocate each tensor using its source storage type or explicitly convert the non-Q8 tensors instead of deriving one global type from `blk.0.attn_q.weight`.</violation>
<violation number="2" location="server/src/qwen3/qwen3_loader.cpp:151">
P2: When an F16 GGUF is loaded on HIP, this branch accepts it but leaves `out.weight_type` as BF16, so the loader rejects the F16→BF16 copies later. Set `out.weight_type` to F16 for an F16 source or add an explicit F16→BF16 conversion.</violation>
</file>
<file name="optimizations/ooo_spec_lucebox5_cpu/trace_compiled_tool_executor.py">
<violation number="1" location="optimizations/ooo_spec_lucebox5_cpu/trace_compiled_tool_executor.py:100">
P2: When a leaf executor returns a non-object envelope, `.get("ok")` raises an uncaught `AttributeError` and the child emits no protocol response. Check the envelope type before calling `.get()`.</violation>
</file>
<file name="server/src/common/qwen3_tool_predictor_ipc.cpp">
<violation number="1" location="server/src/common/qwen3_tool_predictor_ipc.cpp:169">
P1: When native prediction starts, the daemon inherits every non-CLOEXEC descriptor held by the server and target backend. That allows the predictor process to retain or inspect model, tool, or server IPC descriptors, so this path does not provide the promised descriptor privacy. Launch the predictor with a close-on-exec descriptor policy or an explicit inherited-descriptor allowlist before enabling this lane.</violation>
</file>
<file name="server/src/server/semantic_tool_hint.cpp">
<violation number="1" location="server/src/server/semantic_tool_hint.cpp:109">
P3: `parse_content_call` scans every byte of predictor `content`, and for each position holding `{` it runs a full `json::parse(content.begin()+offset, content.end(), nullptr, false)` over the rest of the string. This is O(n²) on unpredictable sidecar output and can be triggered on arbitrary predictor text (HTTP fallback path in `parse_semantic_tool_prediction`). It also accepts the first embedded JSON object as the tool call with no envelope/delimiter requirement, so a response whose real call is not the first object (or that has trailing prose after the object) silently falls through to the authoritative call, defeating the speculative fast path. The per-offset full-parse is the main cost; tail after the object also makes most scan positions fail.</violation>
<violation number="2" location="server/src/server/semantic_tool_hint.cpp:243">
P2: When a sidecar returns multiple `tool_calls` plus content containing one JSON call, this condition skips the array and accepts the content. Reject non-single `tool_calls` responses instead of selecting one representation, so malformed predictions cannot start speculation.</violation>
<violation number="3" location="server/src/server/semantic_tool_hint.cpp:377">
P2: When `tool_choice` is `"none"`, the native prompt still asks for functions, so it can launch an allowlisted speculative call that authoritative decoding will always cancel. Return an opt-out/error prompt for `none` before scheduling prediction.</violation>
</file>
<file name="optimizations/ooo_spec_lucebox5_cpu/benchmark_trace_compiled_workflows.py">
<violation number="1" location="optimizations/ooo_spec_lucebox5_cpu/benchmark_trace_compiled_workflows.py:617">
P1: When `--tool-cpus` differs from the server configuration, the benchmark compares different CPU lanes and can run authoritative tools on model CPUs. Require `tool_speculation.tool_cpu_affinity == args.tool_cpus` before measuring.</violation>
<violation number="2" location="optimizations/ooo_spec_lucebox5_cpu/benchmark_trace_compiled_workflows.py:977">
P1: The final-answer gate never consumes the generated tool result because `post_final()` is context-free and receives the expected receipt directly. Send the accumulated assistant/tool conversation to the final turn and validate that response instead.</violation>
<violation number="3" location="optimizations/ooo_spec_lucebox5_cpu/benchmark_trace_compiled_workflows.py:1453">
P2: When either trace path is customized, the benchmark and executor read different files because the executor paths are never propagated. Require matching executor environment at launch or reject non-default paths before running.</violation>
</file>
<file name="server/src/common/backend_ipc.cpp">
<violation number="1" location="server/src/common/backend_ipc.cpp:452">
P2: The new ownership/mode and lstat checks are applied in the shared `BackendIpcProcess::init_work_dir`, so they tighten every backend-IPC mode (remote DFlash draft, PFlash compress, target shards, moe-expert-compute), not just the new Qwen3 predictor. Any existing deployment that passes a user-supplied `--remote-*-work-dir` that is a symlink (previously accepted via `stat` + `S_ISDIR`) or is owned by a different euid / has mode other than exactly 0700 (e.g. a shared scratch dir) now hard-fails `BackendIpcProcess::start` at startup. If this hardening is intended only for the predictor lane, scope it to that caller or document the cross-mode behavior change for existing multi-GPU deployments that already configure `work_dir`.</violation>
</file>
<file name="optimizations/ooo_spec_lucebox5_cpu/dflash_server_native_tool_predictor_wrapper.sh">
<violation number="1" location="optimizations/ooo_spec_lucebox5_cpu/dflash_server_native_tool_predictor_wrapper.sh:29">
P2: When an operator sets the valid cache-disable value `PREFIX_CACHE_SLOTS_OVERRIDE=0`, this wrapper rejects it before starting the server. Accept zero in the override regex so the wrapper can pass `--prefix-cache-slots 0`.</violation>
<violation number="2" location="optimizations/ooo_spec_lucebox5_cpu/dflash_server_native_tool_predictor_wrapper.sh:45">
P2: When a binary override exists but is not an executable regular file, this preflight accepts it and startup fails later with a generic exec/IPC error. Check the two binaries with `-f && -x` and the model with `-f` before launching.</violation>
</file>
<file name="optimizations/ooo_spec_lucebox5_cpu/README.md">
<violation number="1" location="optimizations/ooo_spec_lucebox5_cpu/README.md:88">
P3: The override advice here conflicts with the next sentence. This paragraph tells operators to override placement with `PREDICTOR_MODEL`/`PREDICTOR_GPU`/`PREDICTOR_MAX_CTX`/`PREDICTOR_MAX_TOKENS`/`PREDICTOR_TIMEOUT_MS` env vars, but immediately notes the qualified launcher clears ambient variables (which is why the `candidate-build` symlink is needed as the durable override). If that launcher clears ambient env, those `PREDICTOR_*` overrides are discarded when launching via `run_native_cpu_server_lucebox5.sh`, so the documented override silently does not take effect through the qualified path. Clarify that the `PREDICTOR_*` overrides apply when launching the wrapper directly, or state how to pass them so they survive the launcher.</violation>
</file>
<file name="server/test/smoke_qwen3_tool_predictor_ipc.cpp">
<violation number="1" location="server/test/smoke_qwen3_tool_predictor_ipc.cpp:190">
P3: The batch gate returns success when names match but does not require exact argument matches, even though exact_matches is already tracked and reported. With the current baseline at 9/12 exact args, this automation would still exit 0 on an argument-canonicalization regression, which is exactly the safety property this PR emphasizes. Gate on exact_matches == cases.size() (or assert the expected baseline explicitly) so the check protects the exact-match commit guarantee.</violation>
</file>
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
There was a problem hiding this comment.
2 issues found across 38 files
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="server/src/server/chat_template.cpp">
<violation number="1" location="server/src/server/chat_template.cpp:390">
P2: The new `tool_call_required` parameter is documented (header) as strengthening the template for OpenAI `tool_choice="required"` and forced-function requests, and both call sites in http_server.cpp pass `tool_choice_requires_call(req.tool_choice)`. However it is only honored in the DEEPSEEK4 case (line 390); the QWEN3, LAGUNA, and GEMMA4 branches ignore it entirely. For QWEN3 — the default/primary architecture for the workflows targeted by this PR — a `tool_choice="required"` or forced-function request produces the same prompt as a normal request, and its preamble even says "If there is no function call available, answer the question like normal", so the model is not actually forced to emit a call. Wire the flag into the other tool-capable branches (at minimum QWEN3, and LAGUNA's non-thinking/thinking tool-block) so the required-call contract holds across architectures.</violation>
</file>
<file name="server/src/server/tool_speculation.cpp">
<violation number="1" location="server/src/server/tool_speculation.cpp:779">
P2: The FD-isolation guarantee is only enforced on glibc >= 2.34. `addclosefrom_np` is guarded by `__GLIBC_PREREQ(2, 34)`, so on glibc 2.17-2.33, musl-based Linux, or any non-glibc POSIX build the child executor inherits every open descriptor from the long-running server (listening sockets, live client connections, model IPC pipes, accelerator descriptors) — directly contradicting the header comment "The executor receives only stdin/stdout/stderr" and the PR's privacy claim. The failure is silent: the code compiles and launches without any FD isolation.</violation>
</file>
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
| def finish_executor(handle: dict[str, Any], timeout: float) -> dict[str, Any]: | ||
| process: subprocess.Popen[str] = handle["process"] | ||
| try: | ||
| stdout, stderr = process.communicate(timeout=timeout) |
There was a problem hiding this comment.
P1: When model generation consumes the configured timeout, finish_executor grants the tool another full timeout because communicate starts timing too late. Compute the remaining timeout from handle["started"] before waiting.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At optimizations/ooo_spec_lucebox5_cpu/benchmark_cpu_tool_speculation.py, line 334:
<comment>When model generation consumes the configured timeout, `finish_executor` grants the tool another full timeout because `communicate` starts timing too late. Compute the remaining timeout from `handle["started"]` before waiting.</comment>
<file context>
@@ -0,0 +1,1409 @@
+def finish_executor(handle: dict[str, Any], timeout: float) -> dict[str, Any]:
+ process: subprocess.Popen[str] = handle["process"]
+ try:
+ stdout, stderr = process.communicate(timeout=timeout)
+ except subprocess.TimeoutExpired:
+ process.kill()
</file context>
| expected_affinity.end()); | ||
| } | ||
| const std::vector<int> affinity = observed_affinity(); | ||
| if (!expected_affinity.empty() && affinity != expected_affinity) { |
There was a problem hiding this comment.
P1: When this executor is configured without a CPU lane, the empty expected mask bypasses isolation and the workload competes with model decoding. Require a non-empty expected affinity and fail closed when it is absent.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At optimizations/ooo_spec_lucebox5_cpu/cpu_sparse_tool_executor.cpp, line 179:
<comment>When this executor is configured without a CPU lane, the empty expected mask bypasses isolation and the workload competes with model decoding. Require a non-empty expected affinity and fail closed when it is absent.</comment>
<file context>
@@ -0,0 +1,259 @@
+ expected_affinity.end());
+ }
+ const std::vector<int> affinity = observed_affinity();
+ if (!expected_affinity.empty() && affinity != expected_affinity) {
+ throw std::runtime_error("observed CPU affinity does not match request");
+ }
</file context>
| if (!expected_affinity.empty() && affinity != expected_affinity) { | |
| if (expected_affinity.empty() || affinity != expected_affinity) { |
| if (!arguments.is_object()) { | ||
| throw std::runtime_error("arguments must be an object"); | ||
| } | ||
| const int rows = arguments.contains("rows") |
There was a problem hiding this comment.
P1: When an allowlisted call contains these extra fields, the executor can allocate several GiB despite the small public iterations schema. Reject unknown arguments and keep the qualified sparse shape fixed, rather than letting model-supplied JSON select dimensions and worker count.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At optimizations/ooo_spec_lucebox5_cpu/cpu_sparse_tool_executor.cpp, line 141:
<comment>When an allowlisted call contains these extra fields, the executor can allocate several GiB despite the small public `iterations` schema. Reject unknown arguments and keep the qualified sparse shape fixed, rather than letting model-supplied JSON select dimensions and worker count.</comment>
<file context>
@@ -0,0 +1,259 @@
+ if (!arguments.is_object()) {
+ throw std::runtime_error("arguments must be an object");
+ }
+ const int rows = arguments.contains("rows")
+ ? integer_argument(arguments, "rows", 64, 1 << 20) : kRows;
+ const int nonzeros = arguments.contains("nonzeros_per_row")
</file context>
| } | ||
| } | ||
| if (wtype == GGML_TYPE_Q8_0) { | ||
| out.weight_type = GGML_TYPE_Q8_0; |
There was a problem hiding this comment.
P1: When the compact GGUF has Q8_0 projection weights but BF16 embeddings or output weights, this assignment applies Q8_0 to every 2-D tensor and the drafter fails to load. Allocate each tensor using its source storage type or explicitly convert the non-Q8 tensors instead of deriving one global type from blk.0.attn_q.weight.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At server/src/qwen3/qwen3_loader.cpp, line 150:
<comment>When the compact GGUF has Q8_0 projection weights but BF16 embeddings or output weights, this assignment applies Q8_0 to every 2-D tensor and the drafter fails to load. Allocate each tensor using its source storage type or explicitly convert the non-Q8 tensors instead of deriving one global type from `blk.0.attn_q.weight`.</comment>
<file context>
@@ -134,16 +134,28 @@ bool load_qwen3_drafter_model(const std::string & path,
}
}
+ if (wtype == GGML_TYPE_Q8_0) {
+ out.weight_type = GGML_TYPE_Q8_0;
+ } else if (wtype != GGML_TYPE_BF16 && wtype != GGML_TYPE_F16) {
+ set_last_error(std::string("unsupported Qwen3-0.6B weight type: ") +
</file context>
| launch.work_dir = work_dir; | ||
| launch.args.push_back("--target-gpu=" + std::to_string(std::max(0, gpu))); | ||
| launch.args.push_back("--max-ctx=" + std::to_string(max_ctx)); | ||
| if (!process_.start(launch)) { |
There was a problem hiding this comment.
P1: When native prediction starts, the daemon inherits every non-CLOEXEC descriptor held by the server and target backend. That allows the predictor process to retain or inspect model, tool, or server IPC descriptors, so this path does not provide the promised descriptor privacy. Launch the predictor with a close-on-exec descriptor policy or an explicit inherited-descriptor allowlist before enabling this lane.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At server/src/common/qwen3_tool_predictor_ipc.cpp, line 169:
<comment>When native prediction starts, the daemon inherits every non-CLOEXEC descriptor held by the server and target backend. That allows the predictor process to retain or inspect model, tool, or server IPC descriptors, so this path does not provide the promised descriptor privacy. Launch the predictor with a close-on-exec descriptor policy or an explicit inherited-descriptor allowlist before enabling this lane.</comment>
<file context>
@@ -0,0 +1,274 @@
+ launch.work_dir = work_dir;
+ launch.args.push_back("--target-gpu=" + std::to_string(std::max(0, gpu)));
+ launch.args.push_back("--max-ctx=" + std::to_string(max_ctx));
+ if (!process_.start(launch)) {
+ std::fprintf(stderr, "[tool-predictor-ipc] backend process start failed\n");
+ return false;
</file context>
| predictor.max_tokens); | ||
| const json tools = req.tools; | ||
| const auto native = native_semantic_predictor_; | ||
| req.semantic_tool_prediction = std::async( |
There was a problem hiding this comment.
P2: launch_semantic_tool_prediction shares one NativeSemanticToolPredictor instance (native_semantic_predictor_) across every request and invokes native->predict(payload, tools) from a fresh std::async thread per request. predict is non-const and the object owns a Qwen3ToolPredictorIpcClient ipc_ with no visible synchronization; concurrent tool-using requests can therefore call predict concurrently on the same IPC client. If that client is not internally thread-safe, this is a data race. Additionally, spawning two std::async(std::launch::async) threads per tool request is unbounded under concurrency.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At server/src/server/http_server.cpp, line 2467:
<comment>`launch_semantic_tool_prediction` shares one `NativeSemanticToolPredictor` instance (`native_semantic_predictor_`) across every request and invokes `native->predict(payload, tools)` from a fresh `std::async` thread per request. `predict` is non-const and the object owns a `Qwen3ToolPredictorIpcClient ipc_` with no visible synchronization; concurrent tool-using requests can therefore call `predict` concurrently on the same IPC client. If that client is not internally thread-safe, this is a data race. Additionally, spawning two `std::async(std::launch::async)` threads per tool request is unbounded under concurrency.</comment>
<file context>
@@ -1969,6 +2434,136 @@ void HttpServer::log_parsed_request(const ParsedRequest & req) const {
+ predictor.max_tokens);
+ const json tools = req.tools;
+ const auto native = native_semantic_predictor_;
+ req.semantic_tool_prediction = std::async(
+ std::launch::async,
+ [predictor, payload, tools, native]() {
</file context>
| result.ok() ? nullptr : "generation_failed"; | ||
| if (req.stream && !client_disconnected) { | ||
| auto final_chunks = emitter.emit_finish(completion_tokens, &gen_timings); | ||
| if (auto metadata = finish_tool_speculation( |
There was a problem hiding this comment.
P2: In overlap mode (the default for HTTP predictors, and the experimental native overlap schedule), launch_semantic_tool_prediction starts the predictor on a std::async worker before the request is enqueued, then finish_tool_speculation calls req.automatic_tool_speculation.get() / req.semantic_tool_prediction.get(). If generation finishes before the prediction (e.g. the model returns a plain answer with no tool call, or a short response), .get() blocks the worker thread for the remaining predictor time (up to --tool-hint-timeout-ms, default 2000 ms). This delays that response and, because the worker thread is shared, stalls other queued jobs. The overlap schedule is supposed to run generation and prediction concurrently, so blocking on the prediction here partially negates it.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At server/src/server/http_server.cpp, line 4572:
<comment>In overlap mode (the default for HTTP predictors, and the experimental native overlap schedule), `launch_semantic_tool_prediction` starts the predictor on a `std::async` worker before the request is enqueued, then `finish_tool_speculation` calls `req.automatic_tool_speculation.get()` / `req.semantic_tool_prediction.get()`. If generation finishes before the prediction (e.g. the model returns a plain answer with no tool call, or a short response), `.get()` blocks the worker thread for the remaining predictor time (up to `--tool-hint-timeout-ms`, default 2000 ms). This delays that response and, because the worker thread is shared, stalls other queued jobs. The overlap schedule is supposed to run generation and prediction concurrently, so blocking on the prediction here partially negates it.</comment>
<file context>
@@ -3882,22 +4512,95 @@ void HttpServer::process_job(ServerJob * job) {
+ result.ok() ? nullptr : "generation_failed";
if (req.stream && !client_disconnected) {
auto final_chunks = emitter.emit_finish(completion_tokens, &gen_timings);
+ if (auto metadata = finish_tool_speculation(
+ generation_cancel_reason)) {
+ const std::string extension = render_tool_speculation_sse(
</file context>
|
|
||
| The launcher defaults to Qwen3-0.6B Q8_0 on predictor GPU 1. Override placement | ||
| with `PREDICTOR_MODEL`, `PREDICTOR_GPU`, `PREDICTOR_MAX_CTX`, | ||
| `PREDICTOR_MAX_TOKENS`, and `PREDICTOR_TIMEOUT_MS`. The adjacent |
There was a problem hiding this comment.
P3: The override advice here conflicts with the next sentence. This paragraph tells operators to override placement with PREDICTOR_MODEL/PREDICTOR_GPU/PREDICTOR_MAX_CTX/PREDICTOR_MAX_TOKENS/PREDICTOR_TIMEOUT_MS env vars, but immediately notes the qualified launcher clears ambient variables (which is why the candidate-build symlink is needed as the durable override). If that launcher clears ambient env, those PREDICTOR_* overrides are discarded when launching via run_native_cpu_server_lucebox5.sh, so the documented override silently does not take effect through the qualified path. Clarify that the PREDICTOR_* overrides apply when launching the wrapper directly, or state how to pass them so they survive the launcher.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At optimizations/ooo_spec_lucebox5_cpu/README.md, line 88:
<comment>The override advice here conflicts with the next sentence. This paragraph tells operators to override placement with `PREDICTOR_MODEL`/`PREDICTOR_GPU`/`PREDICTOR_MAX_CTX`/`PREDICTOR_MAX_TOKENS`/`PREDICTOR_TIMEOUT_MS` env vars, but immediately notes the qualified launcher clears ambient variables (which is why the `candidate-build` symlink is needed as the durable override). If that launcher clears ambient env, those `PREDICTOR_*` overrides are discarded when launching via `run_native_cpu_server_lucebox5.sh`, so the documented override silently does not take effect through the qualified path. Clarify that the `PREDICTOR_*` overrides apply when launching the wrapper directly, or state how to pass them so they survive the launcher.</comment>
<file context>
@@ -0,0 +1,137 @@
+
+The launcher defaults to Qwen3-0.6B Q8_0 on predictor GPU 1. Override placement
+with `PREDICTOR_MODEL`, `PREDICTOR_GPU`, `PREDICTOR_MAX_CTX`,
+`PREDICTOR_MAX_TOKENS`, and `PREDICTOR_TIMEOUT_MS`. The adjacent
+`candidate-build` symlink in the wrapper selects a build even though the
+qualified launcher clears ambient variables.
</file context>
| for (size_t offset = 0; offset < content.size(); ++offset) { | ||
| if (content[offset] != '{') continue; | ||
| try { | ||
| const auto value = json::parse( |
There was a problem hiding this comment.
P3: parse_content_call scans every byte of predictor content, and for each position holding { it runs a full json::parse(content.begin()+offset, content.end(), nullptr, false) over the rest of the string. This is O(n²) on unpredictable sidecar output and can be triggered on arbitrary predictor text (HTTP fallback path in parse_semantic_tool_prediction). It also accepts the first embedded JSON object as the tool call with no envelope/delimiter requirement, so a response whose real call is not the first object (or that has trailing prose after the object) silently falls through to the authoritative call, defeating the speculative fast path. The per-offset full-parse is the main cost; tail after the object also makes most scan positions fail.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At server/src/server/semantic_tool_hint.cpp, line 109:
<comment>`parse_content_call` scans every byte of predictor `content`, and for each position holding `{` it runs a full `json::parse(content.begin()+offset, content.end(), nullptr, false)` over the rest of the string. This is O(n²) on unpredictable sidecar output and can be triggered on arbitrary predictor text (HTTP fallback path in `parse_semantic_tool_prediction`). It also accepts the first embedded JSON object as the tool call with no envelope/delimiter requirement, so a response whose real call is not the first object (or that has trailing prose after the object) silently falls through to the authoritative call, defeating the speculative fast path. The per-offset full-parse is the main cost; tail after the object also makes most scan positions fail.</comment>
<file context>
@@ -0,0 +1,500 @@
+ for (size_t offset = 0; offset < content.size(); ++offset) {
+ if (content[offset] != '{') continue;
+ try {
+ const auto value = json::parse(
+ content.begin() + static_cast<std::ptrdiff_t>(offset),
+ content.end(), nullptr, false);
</file context>
| {"wall_p50_ms", wall_p50}, | ||
| }; | ||
| std::printf("%s\n", summary.dump().c_str()); | ||
| return valid == cases.size() && name_matches == cases.size() ? 0 : 1; |
There was a problem hiding this comment.
P3: The batch gate returns success when names match but does not require exact argument matches, even though exact_matches is already tracked and reported. With the current baseline at 9/12 exact args, this automation would still exit 0 on an argument-canonicalization regression, which is exactly the safety property this PR emphasizes. Gate on exact_matches == cases.size() (or assert the expected baseline explicitly) so the check protects the exact-match commit guarantee.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At server/test/smoke_qwen3_tool_predictor_ipc.cpp, line 190:
<comment>The batch gate returns success when names match but does not require exact argument matches, even though exact_matches is already tracked and reported. With the current baseline at 9/12 exact args, this automation would still exit 0 on an argument-canonicalization regression, which is exactly the safety property this PR emphasizes. Gate on exact_matches == cases.size() (or assert the expected baseline explicitly) so the check protects the exact-match commit guarantee.</comment>
<file context>
@@ -0,0 +1,191 @@
+ {"wall_p50_ms", wall_p50},
+ };
+ std::printf("%s\n", summary.dump().c_str());
+ return valid == cases.size() && name_matches == cases.size() ? 0 : 1;
+}
</file context>
| return valid == cases.size() && name_matches == cases.size() ? 0 : 1; | |
| return valid == cases.size() && name_matches == cases.size() && | |
| exact_matches == cases.size() ? 0 : 1; |
There was a problem hiding this comment.
All reported issues were addressed across 38 files (changes from recent commits).
Not reviewed (too large): optimizations/ooo_spec_lucebox5_cpu/results/multiturn-cached-wordref-production-6tasks.json (~5,595 lines) - if these are generated or fixture files, add them to ignored paths to exclude them from future reviews.
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
There was a problem hiding this comment.
All reported issues were addressed across 16 files (changes from recent commits).
Tip: Review your code locally with the cubic CLI to iterate faster.
Re-trigger cubic
There was a problem hiding this comment.
All reported issues were addressed across 16 files (changes from recent commits).
Tip: Review your code locally with the cubic CLI to iterate faster.
Re-trigger cubic
There was a problem hiding this comment.
All reported issues were addressed across 3 files (changes from recent commits).
Tip: Review your code locally with the cubic CLI to iterate faster.
Re-trigger cubic
…, lane hardening Measured on real tools the pre-generation predictor alone does not pay back (3/17 hits on live public APIs, 0/75 on terminal-bench; ~0.5 s serial cost per turn). Keep every existing lane and add model-agnostic mechanisms that do: - --early-dispatch: launch every allowlisted tool call through the isolated executor lane the moment its call block closes in the token stream (<function_call>, <tool_call>, <function=> wrappers; nested blocks once). N calls per response, each committed on exact canonical match against the parsed authoritative calls; results returned as dflash_early_dispatch (JSON body) / dflash_tool_speculation.early_dispatch (SSE). Authoritative launches need no measured resource profile; --tool-spec-profile is now optional and only gates predictor-based (confidence < 1) speculation. - --end-turn-snapshot: after tool-enabled generations snapshot prompt+output into the inline prefix cache so the next turn restores the conversation and prefills one delta. DeepSeek-V4 continuation snapshots store no final logits / drafter window (DFLASH_DS4_SNAPSHOT_STALE_LOGITS=1, set by the flag); the server never restores them at full prompt length. - Executor result budget counts from the commit (authoritative call known), with a 20x absolute lifetime cap, so slow target turns cannot expire a finished result. - Native predictor daemon relaunches lazily (30 s cooldown) after a timeout or transport failure instead of staying off for the server lifetime. - Wrapper toggles EARLY_DISPATCH / END_TURN_SNAPSHOT / PREDICTOR; README with the measured numbers; real-API and terminal-bench harnesses + artifacts. Unit suite: 412/412 (adds closed-block scanner and commit-deadline tests).
…esolved server-side) Calls that reference earlier calls' results are held back from the stream, substituted once their inputs resolve and executed wave by wave on the same isolated lane at resolve time; entries report dependencies, resolved arguments, wave and result. Placeholder paths must start with a letter so money-like text is never treated as a reference. Unit suite 413/413.
There was a problem hiding this comment.
18 issues found across 32 files (changes from recent commits).
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="server/src/server/http_server.h">
<violation number="1" location="server/src/server/http_server.h:480">
P1: When an allowlisted tool legitimately returns JSON `null`, dependent calls are skipped as failed. Track result availability separately from the JSON value instead of using `is_null()` as the success sentinel.</violation>
</file>
<file name="optimizations/ooo_spec_lucebox5_cpu/realapi/dag_bench.py">
<violation number="1" location="optimizations/ooo_spec_lucebox5_cpu/realapi/dag_bench.py:81">
P2: When a DAG references a nested result field such as `$1.meta.latitude`, `resolve_args` does not resolve the path. Parse the complete dotted path and traverse the result value for both whole and embedded placeholders before executing the dependent call.</violation>
<violation number="2" location="optimizations/ooo_spec_lucebox5_cpu/realapi/dag_bench.py:270">
P2: When running `d09`, a valid summary-only answer with no digits is marked incorrect, and the completion heuristic may spend up to four extra model turns nudging it for numbers. Make completion and correctness validation task-specific instead of unconditionally requiring a digit.</violation>
</file>
<file name="optimizations/ooo_spec_lucebox5_cpu/realapi/rest_tools.py">
<violation number="1" location="optimizations/ooo_spec_lucebox5_cpu/realapi/rest_tools.py:84">
P2: When generation crosses a weather or rate update, an exact name-and-arguments match commits this earlier live snapshot as the current result. Add a freshness/age check before committing these tools, or exclude time-varying tools from speculative hits.</violation>
<violation number="2" location="optimizations/ooo_spec_lucebox5_cpu/realapi/rest_tools.py:124">
P1: When a REST request fails, this line returns `ok: false` inside the raw result, but the executor wraps every raw result in an outer `ok: true`; the server therefore reports a speculative hit for an error. Propagate raw failures as executor failures, or have the executor reject results whose `result.ok` is false, so clients retry through the authoritative path.</violation>
</file>
<file name="server/src/deepseek4/deepseek4_backend.cpp">
<violation number="1" location="server/src/deepseek4/deepseek4_backend.cpp:2270">
P2: When a stale snapshot is restored with a prompt ending exactly at `snap_pos`, generation skips prefill and fails with `DecodeFailed` because `last_logits_` is empty. Reject stale slots for full-length restores or fall back to a fresh prefill before decoding.</violation>
</file>
<file name="server/src/server/http_server.cpp">
<violation number="1" location="server/src/server/http_server.cpp:4219">
P2: When an earlier wrapper is unparseable or contains multiple calls, this shifts later `$k.path` references and can reject a valid dependent call as `unresolvable_dependency`. Track a separate 1-based parsed-call index and map it to the corresponding entry.</violation>
<violation number="2" location="server/src/server/http_server.cpp:4222">
P2: When two authoritative calls have the same canonical name and arguments but different IDs, this marks the second as `duplicate_call` and never launches it. Keep one attempt per authoritative call and match each result to its corresponding ID instead of deduplicating by arguments.</violation>
</file>
<file name="server/src/server/tool_speculation.h">
<violation number="1" location="server/src/server/tool_speculation.h:148">
P1: When a client supplies `tool_speculation.confidence: 1.0` without a resource profile, this new `enabled()` condition admits it and `ToolSpeculationAttempt` launches it before target generation. Do not use confidence as provenance; distinguish request predictions from model-emitted early-dispatch calls before bypassing the profile gate.</violation>
</file>
<file name="optimizations/ooo_spec_lucebox5_cpu/realapi/rest_summary.py">
<violation number="1" location="optimizations/ooo_spec_lucebox5_cpu/realapi/rest_summary.py:15">
P3: The per-task ratio (c["wall_ms"] / s["wall_ms"]) and the aggregate (ct/st) divide by spec wall time with no zero guard. A spec run recording 0 wall_ms aborts the whole summary. Guard before dividing.</violation>
<violation number="2" location="optimizations/ooo_spec_lucebox5_cpu/realapi/rest_summary.py:19">
P3: statistics.median(ratios) and statistics.mean(ratios) raise StatisticsError when ratios is empty, which happens whenever no task has both a control and a spec arm. Guard for the empty case before computing median/mean.</violation>
</file>
<file name="optimizations/ooo_spec_lucebox5_cpu/realapi/rest_bench.py">
<violation number="1" location="optimizations/ooo_spec_lucebox5_cpu/realapi/rest_bench.py:126">
P2: When the model emits a supported `$k.path` dependent call, this map keys the hit by resolved-argument digest while the client hashes raw placeholders, so it misses the completed result and executes an invalid client call. Key early hits by the authoritative `call_id` or by the entry's raw `name` and `arguments` template.</violation>
<violation number="2" location="optimizations/ooo_spec_lucebox5_cpu/realapi/rest_bench.py:151">
P2: Because `expect` contains words copied from each prompt, a model can answer without any tool call and still be counted correct, invalidating the accuracy and control/spec comparison. Require successful tool execution and validate returned values rather than checking prompt keywords only.</violation>
</file>
<file name="optimizations/ooo_spec_lucebox5_cpu/realapi/launch_server.sh">
<violation number="1" location="optimizations/ooo_spec_lucebox5_cpu/realapi/launch_server.sh:20">
P2: This script checks only `pgrep -x dflash_server` before launching, so it will happily start over a GPU already owned by any other workload. The mirrored `run_native_cpu_server_lucebox5.sh` refuses when `/dev/kfd` has an owner (`fuser -s /dev/kfd` → exit 75). Add the `/dev/kfd` guard to avoid corrupting or crashing an in-use GPU card.</violation>
<violation number="2" location="optimizations/ooo_spec_lucebox5_cpu/realapi/launch_server.sh:36">
P2: The script's header advertises the native Qwen3-0.6B predictor and even validates predictor_model, but its `exec env -i` block never sets `PREDICTOR`. Because `env -i` clears the environment and the wrapper defaults `PREDICTOR=0`, the predictor lane stays silently off. The mirrored `run_native_cpu_server_lucebox5.sh` sets `PREDICTOR="1"` here; add it so the advertised predictor is actually enabled.</violation>
</file>
<file name="server/src/server/tool_speculation.cpp">
<violation number="1" location="server/src/server/tool_speculation.cpp:996">
P2: When `--tool-spec-timeout-ms` is near `INT_MAX`, this multiplication overflows before the duration is constructed, producing an invalid absolute deadline and causing executor results to time out immediately. Convert to a wider duration before multiplying or cap the accepted timeout.</violation>
</file>
<file name="optimizations/ooo_spec_lucebox5_cpu/realapi/results/dag_small_4tasks_3arms.json">
<violation number="1" location="optimizations/ooo_spec_lucebox5_cpu/realapi/results/dag_small_4tasks_3arms.json:1267">
P2: The committed s04 `dag` result's final answer is corrupted with U+FFFD replacement characters where digits were dropped ("7���2", "8���0", "1���5th-century", "1���3.9 km/h"), while the same task's `parallel` arm produced the clean text ("2,430 meters", "13.9 km/h"). Only the dag/early-dispatch run is garbled, which contradicts the PR's "lossless" claim for tool-result passthrough. Confirm this is an external-API byte anomaly and not the tool-result encoding being lossy-decoded in the early-dispatch/dependency lane (bytes replaced with U+FFFD), then re-run; a degraded result should not stand as evidence of correctness.</violation>
</file>
<file name="optimizations/ooo_spec_lucebox5_cpu/realapi/restart_server.sh">
<violation number="1" location="optimizations/ooo_spec_lucebox5_cpu/realapi/restart_server.sh:6">
P2: When the `logs/` directory does not exist under the cd'd path, the `> "$log"` redirect fails silently and the server output is discarded. The success grep and the `tail -20 "$log"` error path then read a nonexistent file, so the loop never sees "listening on" and the script burns the full 240x5s window before reporting a misleading `TIMEOUT`. Create the directory before writing the log so the failure is detected at launch.</violation>
</file>
Tip: Review your code locally with the cubic CLI to iterate faster.
Re-trigger cubic
| value = {"error": f"{type(exc).__name__}: {exc}"} | ||
| ok = False | ||
| elapsed_ms = (time.perf_counter() - started) * 1000.0 | ||
| return {"tool_name": name, "call_sha256": call_sha256(name, arguments), "ok": ok, "value": value, "elapsed_ms": elapsed_ms, "side_effects": False} |
There was a problem hiding this comment.
P1: When a REST request fails, this line returns ok: false inside the raw result, but the executor wraps every raw result in an outer ok: true; the server therefore reports a speculative hit for an error. Propagate raw failures as executor failures, or have the executor reject results whose result.ok is false, so clients retry through the authoritative path.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At optimizations/ooo_spec_lucebox5_cpu/realapi/rest_tools.py, line 124:
<comment>When a REST request fails, this line returns `ok: false` inside the raw result, but the executor wraps every raw result in an outer `ok: true`; the server therefore reports a speculative hit for an error. Propagate raw failures as executor failures, or have the executor reject results whose `result.ok` is false, so clients retry through the authoritative path.</comment>
<file context>
@@ -0,0 +1,129 @@
+ value = {"error": f"{type(exc).__name__}: {exc}"}
+ ok = False
+ elapsed_ms = (time.perf_counter() - started) * 1000.0
+ return {"tool_name": name, "call_sha256": call_sha256(name, arguments), "ok": ok, "value": value, "elapsed_ms": elapsed_ms, "side_effects": False}
+
+
</file context>
| @@ -0,0 +1,267 @@ | |||
| // Lossless, confidence-gated speculative tool execution. | |||
There was a problem hiding this comment.
P1: When a client supplies tool_speculation.confidence: 1.0 without a resource profile, this new enabled() condition admits it and ToolSpeculationAttempt launches it before target generation. Do not use confidence as provenance; distinguish request predictions from model-emitted early-dispatch calls before bypassing the profile gate.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At server/src/server/tool_speculation.h, line 148:
<comment>When a client supplies `tool_speculation.confidence: 1.0` without a resource profile, this new `enabled()` condition admits it and `ToolSpeculationAttempt` launches it before target generation. Do not use confidence as provenance; distinguish request predictions from model-emitted early-dispatch calls before bypassing the profile gate.</comment>
<file context>
@@ -139,9 +140,15 @@ struct ToolSpeculationConfig {
bool enabled() const {
- return !executor_path.empty() && !allowed_tools.empty() &&
- !policy.empty();
+ return !executor_path.empty() && !allowed_tools.empty();
+ }
+ bool predictive_enabled() const {
</file context>
| std::string skip_reason; | ||
| std::vector<int> dependencies; // call indices referenced by $k.path | ||
| nlohmann::json resolved_arguments; // after substitution (dependent calls) | ||
| nlohmann::json result; // executor result on hit |
There was a problem hiding this comment.
P1: When an allowlisted tool legitimately returns JSON null, dependent calls are skipped as failed. Track result availability separately from the JSON value instead of using is_null() as the success sentinel.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At server/src/server/http_server.h, line 480:
<comment>When an allowlisted tool legitimately returns JSON `null`, dependent calls are skipped as failed. Track result availability separately from the JSON value instead of using `is_null()` as the success sentinel.</comment>
<file context>
@@ -460,10 +469,27 @@ class HttpServer {
+ std::string skip_reason;
+ std::vector<int> dependencies; // call indices referenced by $k.path
+ nlohmann::json resolved_arguments; // after substitution (dependent calls)
+ nlohmann::json result; // executor result on hit
+ bool done = false;
+ int wave = 0;
</file context>
| cd /home/lucebox5/tbspec | ||
| pkill -x dflash_server 2>/dev/null; sleep 2; pkill -9 -x dflash_server 2>/dev/null | ||
| for i in $(seq 1 30); do pgrep -x dflash_server >/dev/null || break; sleep 1; done | ||
| log=logs/server_$(date +%Y%m%d_%H%M%S).log |
There was a problem hiding this comment.
P2: When the logs/ directory does not exist under the cd'd path, the > "$log" redirect fails silently and the server output is discarded. The success grep and the tail -20 "$log" error path then read a nonexistent file, so the loop never sees "listening on" and the script burns the full 240x5s window before reporting a misleading TIMEOUT. Create the directory before writing the log so the failure is detected at launch.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At optimizations/ooo_spec_lucebox5_cpu/realapi/restart_server.sh, line 6:
<comment>When the `logs/` directory does not exist under the cd'd path, the `> "$log"` redirect fails silently and the server output is discarded. The success grep and the `tail -20 "$log"` error path then read a nonexistent file, so the loop never sees "listening on" and the script burns the full 240x5s window before reporting a misleading `TIMEOUT`. Create the directory before writing the log so the failure is detected at launch.</comment>
<file context>
@@ -0,0 +1,14 @@
+cd /home/lucebox5/tbspec
+pkill -x dflash_server 2>/dev/null; sleep 2; pkill -9 -x dflash_server 2>/dev/null
+for i in $(seq 1 30); do pgrep -x dflash_server >/dev/null || break; sleep 1; done
+log=logs/server_$(date +%Y%m%d_%H%M%S).log
+setsid nohup ./launch_server.sh > "$log" 2>&1 < /dev/null &
+lpid=$!
</file context>
| log=logs/server_$(date +%Y%m%d_%H%M%S).log | |
| mkdir -p logs | |
| log=logs/server_$(date +%Y%m%d_%H%M%S).log |
| for t, a in sorted(by.items()): | ||
| if "control" in a and "spec" in a: | ||
| c, s = a["control"], a["spec"] | ||
| ratio = c["wall_ms"] / s["wall_ms"]; ratios.append(ratio) |
There was a problem hiding this comment.
P3: The per-task ratio (c["wall_ms"] / s["wall_ms"]) and the aggregate (ct/st) divide by spec wall time with no zero guard. A spec run recording 0 wall_ms aborts the whole summary. Guard before dividing.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At optimizations/ooo_spec_lucebox5_cpu/realapi/rest_summary.py, line 15:
<comment>The per-task ratio (c["wall_ms"] / s["wall_ms"]) and the aggregate (ct/st) divide by spec wall time with no zero guard. A spec run recording 0 wall_ms aborts the whole summary. Guard before dividing.</comment>
<file context>
@@ -0,0 +1,34 @@
+for t, a in sorted(by.items()):
+ if "control" in a and "spec" in a:
+ c, s = a["control"], a["spec"]
+ ratio = c["wall_ms"] / s["wall_ms"]; ratios.append(ratio)
+ ct += c["wall_ms"]; st += s["wall_ms"]; hits += s["hits"]; calls += s["calls"]; pred += s["predictor_ms"]
+ twc += c["tool_wait_ms"]; tws += s["tool_wait_ms"]; okc += c["correct"]; oks += s["correct"]; multi += s["multi_call_turns"]
</file context>
| ct += c["wall_ms"]; st += s["wall_ms"]; hits += s["hits"]; calls += s["calls"]; pred += s["predictor_ms"] | ||
| twc += c["tool_wait_ms"]; tws += s["tool_wait_ms"]; okc += c["correct"]; oks += s["correct"]; multi += s["multi_call_turns"] | ||
| print(f"{t:5} {s['calls']:>5} {c['wall_ms']/1000:7.1f} {s['wall_ms']/1000:7.1f} {ratio:6.3f} {s['hits']:>4} {s['predictor_ms']/1000:6.1f} {c['tool_wait_ms']/1000:7.2f} {s['tool_wait_ms']/1000:7.2f} {str(c['correct']):>4} {str(s['correct']):>4} {s['multi_call_turns']:>5} {order[t]}") | ||
| print(f"pairs={len(ratios)} median x{statistics.median(ratios):.3f} mean x{statistics.mean(ratios):.3f} aggregate x{ct/st:.3f} | spec hits {hits}/{calls} calls | predictor {pred/1000:.1f}s total | tool wait control {twc/1000:.1f}s spec {tws/1000:.1f}s | correct control {okc} spec {oks} | multi-call turns {multi}") |
There was a problem hiding this comment.
P3: statistics.median(ratios) and statistics.mean(ratios) raise StatisticsError when ratios is empty, which happens whenever no task has both a control and a spec arm. Guard for the empty case before computing median/mean.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At optimizations/ooo_spec_lucebox5_cpu/realapi/rest_summary.py, line 19:
<comment>statistics.median(ratios) and statistics.mean(ratios) raise StatisticsError when ratios is empty, which happens whenever no task has both a control and a spec arm. Guard for the empty case before computing median/mean.</comment>
<file context>
@@ -0,0 +1,34 @@
+ ct += c["wall_ms"]; st += s["wall_ms"]; hits += s["hits"]; calls += s["calls"]; pred += s["predictor_ms"]
+ twc += c["tool_wait_ms"]; tws += s["tool_wait_ms"]; okc += c["correct"]; oks += s["correct"]; multi += s["multi_call_turns"]
+ print(f"{t:5} {s['calls']:>5} {c['wall_ms']/1000:7.1f} {s['wall_ms']/1000:7.1f} {ratio:6.3f} {s['hits']:>4} {s['predictor_ms']/1000:6.1f} {c['tool_wait_ms']/1000:7.2f} {s['tool_wait_ms']/1000:7.2f} {str(c['correct']):>4} {str(s['correct']):>4} {s['multi_call_turns']:>5} {order[t]}")
+print(f"pairs={len(ratios)} median x{statistics.median(ratios):.3f} mean x{statistics.mean(ratios):.3f} aggregate x{ct/st:.3f} | spec hits {hits}/{calls} calls | predictor {pred/1000:.1f}s total | tool wait control {twc/1000:.1f}s spec {tws/1000:.1f}s | correct control {okc} spec {oks} | multi-call turns {multi}")
+# hits detail + predictor outcomes
+import collections
</file context>
…ue core + prefetch-prefill Remove the pre-generation Qwen3 predictor lane (native IPC daemon, semantic hint parser, HTTP sidecar, its tokenizer/loader/ipc plumbing and tests) and the replay-benchmark bulk: measured on real tools it hit 3/17 calls on live APIs and 0/75 on terminal-bench while costing ~0.5 s of serial predictor time per turn. Client-supplied predictions via the tool_speculation extension keep working; --tool-spec-profile now only gates those. Add --prefetch-prefill: after a tool turn whose early-dispatched results all committed, the next request is fully determined, so the server renders it, restores the end-of-turn snapshot, prefills the delta and caches the KV before the client's next request arrives. Each early-dispatch hit carries tool_message_content, the canonical tool text a client echoes for a guaranteed hit; every failure path falls back silently. optimizations/ now ships one small real-API harness (realapi/) with compact artifacts instead of 20k lines of replay benchmarks. Unit suite 386/386.
…ll prefetched off the critical path
There was a problem hiding this comment.
11 issues found across 53 files (changes from recent commits).
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="optimizations/ooo_spec_lucebox5_cpu/README.md">
<violation number="1" location="optimizations/ooo_spec_lucebox5_cpu/README.md:76">
P2: Following the Reproduce instructions never applies the intended executor or allowlist. launch_server.sh reads TBSPEC_EXECUTOR and TBSPEC_ALLOW (defaulting to the terminal-bench tb_tool_executor.py and `read_file,list_dir,search_files`), and its `env -i` discards the TOOL_SPEC_EXECUTOR/TOOL_SPEC_ALLOW and BUILD_DIR=$PWD/realapi you pass. As written, the server launches with the wrong tools, so rest_bench/dag_bench calls are not allowlisted and the documented measurements cannot be reproduced. Set TBSPEC_EXECUTOR and TBSPEC_ALLOW instead of TOOL_SPEC_*, and drop the ineffective BUILD_DIR.</violation>
</file>
<file name="optimizations/ooo_spec_lucebox5_cpu/realapi/wrapper_dflash_server.sh">
<violation number="1" location="optimizations/ooo_spec_lucebox5_cpu/realapi/wrapper_dflash_server.sh:42">
P2: When an operator sets `LUCE_MMVQ_MAX_NCOLS`, this wrapper discards the explicit dispatch override and forces the q=5 threshold. Preserve the caller's value while using 5 only as the default.</violation>
</file>
<file name="server/src/common/backend_ipc.cpp">
<violation number="1">
P2: When a signal interrupts shutdown, `close()` forgets the still-running child and can leave a zombie. Retry `waitpid` on `EINTR` before clearing `pid_`.</violation>
<violation number="2">
P2: When work-directory creation succeeds but pipe setup or required shared transport validation fails, these early returns leave the owned temporary directory behind. Call `close()` or explicitly remove the owned directory on every post-initialization failure path.</violation>
<violation number="3">
P1: If the daemon stalls before sending its readiness status, `start()` blocks indefinitely on this read and prevents the server from falling back or reporting startup failure. Restore a bounded readiness deadline and terminate the child when it expires.</violation>
</file>
<file name="server/src/server/http_server.cpp">
<violation number="1" location="server/src/server/http_server.cpp:904">
P3: The `/props` predictor metadata now contains unreachable constant branches, which makes the advertised capability contract difficult to audit. Replace these expressions with direct `false`/`json(nullptr)` values or remove the obsolete fields.</violation>
<violation number="2" location="server/src/server/http_server.cpp:3979">
P2: When an early-dispatch response contains a dependent call, `prefetch_next_turn` always returns before prefill because dependent hits never receive `tool_message_content`. Add the same canonical-content assignment in the dependent-hit branch.</violation>
<violation number="3" location="server/src/server/http_server.cpp:4008">
P2: When `prepare_inline_snap` selects the lookup's `restore_slot`, this line frees the source snapshot before prefetch restores from it. Cancel the reservation and skip prefetch when `snap_slot == restore_slot` so the valid end-of-turn cache entry is preserved.</violation>
</file>
<file name="server/src/qwen3/qwen3_backend.cpp">
<violation number="1">
P1: Writing the decoded token one slot too far: in do_decode, `committed` is incremented before this call (see `committed++; cache_.cur_pos = committed;` above), so the just-sampled `next` belongs at zero-based position `committed - 1`, not `committed`. This leaves a hole in the KV cache at the first decoded slot and shifts every subsequent decoded token's K/V one position past its true position (do_step writes K/V at `kv_start` via the cache view and assigns rope positions from `kv_start+i`). The prior `committed - 1` was correct and matches the sibling call sites that write `first` at pre-increment `cur_committed`. Restore `committed - 1` so generation (and any end-of-turn snapshot) reflects the real token sequence.</violation>
</file>
<file name="server/src/server/server_main.cpp">
<violation number="1" location="server/src/server/server_main.cpp:634">
P2: When `--prefetch-prefill` is passed without `--early-dispatch`, startup succeeds but every prefetch is skipped because `output.early_dispatch` remains false. Reject this incomplete flag combination after parsing, including the documented `--end-turn-snapshot` prerequisite, instead of silently disabling the requested optimization.</violation>
</file>
<file name="optimizations/ooo_spec_lucebox5_cpu/realapi/rest_bench.py">
<violation number="1" location="optimizations/ooo_spec_lucebox5_cpu/realapi/rest_bench.py:151">
P3: This change makes the tool-message text differ between the two benchmark arms, contradicting the script's stated design that "the ONLY difference is the request field automatic_tool_speculation." In the spec arm, early-hit tool results are echoed as the server's canonical text (rapidjson key ordering via tool_message_content) while non-hit calls and all control-arm calls use the client's Python json.dumps formatting. The values are identical (a hit is an exact canonical match), so timings are unaffected, but the correctness comparison feeds the model differently formatted tool text in each arm and can confound the paired result. If consistency matters, dump the value with the same compaction/sorting in both arms, or document the intended divergence.</violation>
</file>
Tip: Review your code locally with the cubic CLI to iterate faster.
Re-trigger cubic
| BUILD_DIR=$PWD/realapi TOOL_SPEC_EXECUTOR=$PWD/realapi/rest_tool_executor.py \ | ||
| TOOL_SPEC_ALLOW=geocode_city,get_weather,country_info,wikipedia_summary,exchange_rate \ | ||
| ./realapi/launch_server.sh |
There was a problem hiding this comment.
P2: Following the Reproduce instructions never applies the intended executor or allowlist. launch_server.sh reads TBSPEC_EXECUTOR and TBSPEC_ALLOW (defaulting to the terminal-bench tb_tool_executor.py and read_file,list_dir,search_files), and its env -i discards the TOOL_SPEC_EXECUTOR/TOOL_SPEC_ALLOW and BUILD_DIR=$PWD/realapi you pass. As written, the server launches with the wrong tools, so rest_bench/dag_bench calls are not allowlisted and the documented measurements cannot be reproduced. Set TBSPEC_EXECUTOR and TBSPEC_ALLOW instead of TOOL_SPEC_*, and drop the ineffective BUILD_DIR.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At optimizations/ooo_spec_lucebox5_cpu/README.md, line 76:
<comment>Following the Reproduce instructions never applies the intended executor or allowlist. launch_server.sh reads TBSPEC_EXECUTOR and TBSPEC_ALLOW (defaulting to the terminal-bench tb_tool_executor.py and `read_file,list_dir,search_files`), and its `env -i` discards the TOOL_SPEC_EXECUTOR/TOOL_SPEC_ALLOW and BUILD_DIR=$PWD/realapi you pass. As written, the server launches with the wrong tools, so rest_bench/dag_bench calls are not allowlisted and the documented measurements cannot be reproduced. Set TBSPEC_EXECUTOR and TBSPEC_ALLOW instead of TOOL_SPEC_*, and drop the ineffective BUILD_DIR.</comment>
<file context>
@@ -1,230 +1,87 @@
- ./run_native_cpu_server_lucebox5.sh
-```
+# server (wraps the qualified 0731 launcher's binary selection)
+BUILD_DIR=$PWD/realapi TOOL_SPEC_EXECUTOR=$PWD/realapi/rest_tool_executor.py \
+ TOOL_SPEC_ALLOW=geocode_city,get_weather,country_info,wikipedia_summary,exchange_rate \
+ ./realapi/launch_server.sh
</file context>
| BUILD_DIR=$PWD/realapi TOOL_SPEC_EXECUTOR=$PWD/realapi/rest_tool_executor.py \ | |
| TOOL_SPEC_ALLOW=geocode_city,get_weather,country_info,wikipedia_summary,exchange_rate \ | |
| ./realapi/launch_server.sh | |
| TBSPEC_EXECUTOR=$PWD/realapi/rest_tool_executor.py \ | |
| TBSPEC_ALLOW=geocode_city,get_weather,country_info,wikipedia_summary,exchange_rate \ | |
| ./realapi/launch_server.sh |
| extra_args+=(--prefix-cache-slots "${PREFIX_CACHE_SLOTS_OVERRIDE}") | ||
| fi | ||
| export LD_LIBRARY_PATH="${CANDIDATE_BUILD}/deps/llama.cpp/ggml/src:${CANDIDATE_BUILD}/deps/llama.cpp/ggml/src/ggml-hip:${LD_LIBRARY_PATH:-}" | ||
| export LUCE_MMVQ_MAX_NCOLS=5 |
There was a problem hiding this comment.
P2: When an operator sets LUCE_MMVQ_MAX_NCOLS, this wrapper discards the explicit dispatch override and forces the q=5 threshold. Preserve the caller's value while using 5 only as the default.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At optimizations/ooo_spec_lucebox5_cpu/realapi/wrapper_dflash_server.sh, line 42:
<comment>When an operator sets `LUCE_MMVQ_MAX_NCOLS`, this wrapper discards the explicit dispatch override and forces the q=5 threshold. Preserve the caller's value while using 5 only as the default.</comment>
<file context>
@@ -0,0 +1,43 @@
+ extra_args+=(--prefix-cache-slots "${PREFIX_CACHE_SLOTS_OVERRIDE}")
+fi
+export LD_LIBRARY_PATH="${CANDIDATE_BUILD}/deps/llama.cpp/ggml/src:${CANDIDATE_BUILD}/deps/llama.cpp/ggml/src/ggml-hip:${LD_LIBRARY_PATH:-}"
+export LUCE_MMVQ_MAX_NCOLS=5
+exec "${CANDIDATE_BUILD}/dflash_server" "$@" "${extra_args[@]}"
</file context>
| tokens, prefix_len, false, (int) tokens.size()); | ||
| if (prepared.first < 0 || prepared.second != (int) tokens.size()) return; | ||
| const int snap_slot = prepared.first; | ||
| backend_.snapshot_free(snap_slot); |
There was a problem hiding this comment.
P2: When prepare_inline_snap selects the lookup's restore_slot, this line frees the source snapshot before prefetch restores from it. Cancel the reservation and skip prefetch when snap_slot == restore_slot so the valid end-of-turn cache entry is preserved.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At server/src/server/http_server.cpp, line 4008:
<comment>When `prepare_inline_snap` selects the lookup's `restore_slot`, this line frees the source snapshot before prefetch restores from it. Cancel the reservation and skip prefetch when `snap_slot == restore_slot` so the valid end-of-turn cache entry is preserved.</comment>
<file context>
@@ -4416,6 +3954,87 @@ json HttpServer::resolve_early_dispatch(
+ tokens, prefix_len, false, (int) tokens.size());
+ if (prepared.first < 0 || prepared.second != (int) tokens.size()) return;
+ const int snap_slot = prepared.first;
+ backend_.snapshot_free(snap_slot);
+ GenerateRequest prefetch_request;
+ prefetch_request.prompt = tokens;
</file context>
| } | ||
| } | ||
| for (const json * item : results) { | ||
| if (!item || item->value("tool_message_content", "").empty()) return; |
There was a problem hiding this comment.
P2: When an early-dispatch response contains a dependent call, prefetch_next_turn always returns before prefill because dependent hits never receive tool_message_content. Add the same canonical-content assignment in the dependent-hit branch.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At server/src/server/http_server.cpp, line 3979:
<comment>When an early-dispatch response contains a dependent call, `prefetch_next_turn` always returns before prefill because dependent hits never receive `tool_message_content`. Add the same canonical-content assignment in the dependent-hit branch.</comment>
<file context>
@@ -4416,6 +3954,87 @@ json HttpServer::resolve_early_dispatch(
+ }
+ }
+ for (const json * item : results) {
+ if (!item || item->value("tool_message_content", "").empty()) return;
+ }
+ std::vector<std::string> call_ids;
</file context>
| } | ||
| } else if (std::strcmp(argv[i], "--early-dispatch") == 0) { | ||
| sconfig.early_dispatch = true; | ||
| } else if (std::strcmp(argv[i], "--prefetch-prefill") == 0) { |
There was a problem hiding this comment.
P2: When --prefetch-prefill is passed without --early-dispatch, startup succeeds but every prefetch is skipped because output.early_dispatch remains false. Reject this incomplete flag combination after parsing, including the documented --end-turn-snapshot prerequisite, instead of silently disabling the requested optimization.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At server/src/server/server_main.cpp, line 634:
<comment>When `--prefetch-prefill` is passed without `--early-dispatch`, startup succeeds but every prefetch is skipped because `output.early_dispatch` remains false. Reject this incomplete flag combination after parsing, including the documented `--end-turn-snapshot` prerequisite, instead of silently disabling the requested optimization.</comment>
<file context>
@@ -716,6 +631,8 @@ int main(int argc, char ** argv) {
}
} else if (std::strcmp(argv[i], "--early-dispatch") == 0) {
sconfig.early_dispatch = true;
+ } else if (std::strcmp(argv[i], "--prefetch-prefill") == 0) {
+ sconfig.prefetch_prefill = true;
} else if (std::strcmp(argv[i], "--end-turn-snapshot") == 0) {
</file context>
| config.tool_speculation.enabled() && | ||
| false}, | ||
| {"prediction_source", | ||
| false |
There was a problem hiding this comment.
P3: The /props predictor metadata now contains unreachable constant branches, which makes the advertised capability contract difficult to audit. Replace these expressions with direct false/json(nullptr) values or remove the obsolete fields.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At server/src/server/http_server.cpp, line 904:
<comment>The `/props` predictor metadata now contains unreachable constant branches, which makes the advertised capability contract difficult to audit. Replace these expressions with direct `false`/`json(nullptr)` values or remove the obsolete fields.</comment>
<file context>
@@ -1257,22 +899,22 @@ json build_props_body(const ServerConfig & config,
+ false},
{"prediction_source",
- config.semantic_tool_predictor.native_enabled()
+ false
? json("native-qwen3")
- : config.semantic_tool_predictor.http_enabled()
</file context>
| for i, (cid, name, args) in enumerate(parsed): | ||
| sha = rest_tools.call_sha256(name, args) if args is not None else None | ||
| # Echo the server's canonical tool text so prefetch-prefill hits. | ||
| content = early_content.get(sha) or rest_tools.format_result(results[i]) |
There was a problem hiding this comment.
P3: This change makes the tool-message text differ between the two benchmark arms, contradicting the script's stated design that "the ONLY difference is the request field automatic_tool_speculation." In the spec arm, early-hit tool results are echoed as the server's canonical text (rapidjson key ordering via tool_message_content) while non-hit calls and all control-arm calls use the client's Python json.dumps formatting. The values are identical (a hit is an exact canonical match), so timings are unaffected, but the correctness comparison feeds the model differently formatted tool text in each arm and can confound the paired result. If consistency matters, dump the value with the same compaction/sorting in both arms, or document the intended divergence.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At optimizations/ooo_spec_lucebox5_cpu/realapi/rest_bench.py, line 151:
<comment>This change makes the tool-message text differ between the two benchmark arms, contradicting the script's stated design that "the ONLY difference is the request field automatic_tool_speculation." In the spec arm, early-hit tool results are echoed as the server's canonical text (rapidjson key ordering via tool_message_content) while non-hit calls and all control-arm calls use the client's Python json.dumps formatting. The values are identical (a hit is an exact canonical match), so timings are unaffected, but the correctness comparison feeds the model differently formatted tool text in each arm and can confound the paired result. If consistency matters, dump the value with the same compaction/sorting in both arms, or document the intended divergence.</comment>
<file context>
@@ -145,7 +146,10 @@ def _exec(i):
- messages.append({"role": "tool", "tool_call_id": cid, "content": rest_tools.format_result(results[i])})
+ sha = rest_tools.call_sha256(name, args) if args is not None else None
+ # Echo the server's canonical tool text so prefetch-prefill hits.
+ content = early_content.get(sha) or rest_tools.format_result(results[i])
+ messages.append({"role": "tool", "tool_call_id": cid, "content": content})
turns.append(turn)
</file context>
- Prefetch-prefill yields to real traffic: skipped entirely when a request is already queued, and cancelled at the next prefill chunk boundary when one arrives mid-prefetch (logged as 'yielded to a queued request'). A waiting client never pays for speculative prefill. - Server-wide executor saturation cap (--tool-spec-max-executors, default 16): attempts beyond the cap are deferred with reason executor_saturated instead of spawning unbounded children under concurrent load; slot accounting is exact across the reap, terminate and destructor paths. - Unit suite 387/387 (adds the saturation-cap test). Verified live: paired REST bench with a competing client every 12 s on the same server — no crashes, competitor 10/10 correct, bench answers identical.
There was a problem hiding this comment.
3 issues found across 6 files (changes from recent commits).
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="server/src/server/server_main.cpp">
<violation number="1" location="server/src/server/server_main.cpp:222">
P3: The CLI validates `--tool-spec-max-executors` as `>= 0`, and the executor lane treats 0 as "no cap" (tool_speculation.cpp only enforces the cap when `max_concurrent_executors > 0`). The usage text documents only the default (16) and never says that 0 disables the cap, so an operator can pass 0 expecting zero executors and instead get unlimited concurrency. Document that 0 = unlimited in the flag help.</violation>
</file>
<file name="server/src/server/http_server.cpp">
<violation number="1" location="server/src/server/http_server.cpp:4043">
P3: When a queued request cancels prefill, the backend records cancellation on its copied `DaemonIO`, so `prefetch_io.cancelled` remains false and this branch never logs that prefetch yielded. Check the queue state or propagate cancellation through shared state instead.</violation>
</file>
<file name="server/src/server/tool_speculation.cpp">
<violation number="1" location="server/src/server/tool_speculation.cpp:693">
P2: When `terminate_executor(true)` reaps a child during either grace wait, it returns before releasing the newly acquired executor slot. Subsequent dependency waves can therefore receive `executor_saturated` even though no child is running; release the slot on every terminal return, including the analogous wait-error path in `collect_executor_result`.</violation>
</file>
Tip: Review your code locally with the cubic CLI to iterate faster.
Re-trigger cubic
|
|
||
| ToolSpeculationAttempt::~ToolSpeculationAttempt() { | ||
| if (!resolved_) terminate_executor(); | ||
| release_executor_slot(); |
There was a problem hiding this comment.
P2: When terminate_executor(true) reaps a child during either grace wait, it returns before releasing the newly acquired executor slot. Subsequent dependency waves can therefore receive executor_saturated even though no child is running; release the slot on every terminal return, including the analogous wait-error path in collect_executor_result.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At server/src/server/tool_speculation.cpp, line 693:
<comment>When `terminate_executor(true)` reaps a child during either grace wait, it returns before releasing the newly acquired executor slot. Subsequent dependency waves can therefore receive `executor_saturated` even though no child is running; release the slot on every terminal return, including the analogous wait-error path in `collect_executor_result`.</comment>
<file context>
@@ -678,8 +681,16 @@ ToolSpeculationAttempt::ToolSpeculationAttempt(
+
ToolSpeculationAttempt::~ToolSpeculationAttempt() {
if (!resolved_) terminate_executor();
+ release_executor_slot();
}
</file context>
| " --tool-spec-timeout-ms <N> Executor result timeout (default: 60000).\n" | ||
| " --early-dispatch Launch allowlisted tool calls as soon as their call block\n" | ||
| " closes in the token stream (results: dflash_early_dispatch).\n" | ||
| " --tool-spec-max-executors <N> Server-wide concurrent executor cap (default: 16).\n" |
There was a problem hiding this comment.
P3: The CLI validates --tool-spec-max-executors as >= 0, and the executor lane treats 0 as "no cap" (tool_speculation.cpp only enforces the cap when max_concurrent_executors > 0). The usage text documents only the default (16) and never says that 0 disables the cap, so an operator can pass 0 expecting zero executors and instead get unlimited concurrency. Document that 0 = unlimited in the flag help.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At server/src/server/server_main.cpp, line 222:
<comment>The CLI validates `--tool-spec-max-executors` as `>= 0`, and the executor lane treats 0 as "no cap" (tool_speculation.cpp only enforces the cap when `max_concurrent_executors > 0`). The usage text documents only the default (16) and never says that 0 disables the cap, so an operator can pass 0 expecting zero executors and instead get unlimited concurrency. Document that 0 = unlimited in the flag help.</comment>
<file context>
@@ -219,6 +219,7 @@ static void print_usage(const char * prog) {
" --tool-spec-timeout-ms <N> Executor result timeout (default: 60000).\n"
" --early-dispatch Launch allowlisted tool calls as soon as their call block\n"
" closes in the token stream (results: dflash_early_dispatch).\n"
+ " --tool-spec-max-executors <N> Server-wide concurrent executor cap (default: 16).\n"
" --prefetch-prefill Prefill the deterministic next tool turn before the\n"
" client requests it (with --early-dispatch + --end-turn-snapshot).\n"
</file context>
| " --tool-spec-max-executors <N> Server-wide concurrent executor cap (default: 16).\n" | |
| " --tool-spec-max-executors <N> Server-wide concurrent executor cap (default: 16; 0 = unlimited).\n" |
| } else { | ||
| backend_.snapshot_free(snap_slot); | ||
| prefix_cache_.abort_inline_snap(snap_slot); | ||
| if (prefetch_io.cancelled) { |
There was a problem hiding this comment.
P3: When a queued request cancels prefill, the backend records cancellation on its copied DaemonIO, so prefetch_io.cancelled remains false and this branch never logs that prefetch yielded. Check the queue state or propagate cancellation through shared state instead.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At server/src/server/http_server.cpp, line 4043:
<comment>When a queued request cancels prefill, the backend records cancellation on its copied `DaemonIO`, so `prefetch_io.cancelled` remains false and this branch never logs that prefetch yielded. Check the queue state or propagate cancellation through shared state instead.</comment>
<file context>
@@ -4032,6 +4040,10 @@ void HttpServer::prefetch_next_turn(
} else {
backend_.snapshot_free(snap_slot);
prefix_cache_.abort_inline_snap(snap_slot);
+ if (prefetch_io.cancelled) {
+ std::fprintf(stderr,
+ "[prefetch] yielded to a queued request\n");
</file context>
Summary
Lossless speculative tool execution for agentic workflows, in ~2.5k lines of server code. The model stays authoritative; speculative work commits only on an exact canonical match, and every failure path falls back silently to the normal request flow.
Three flags, designed to run together:
--early-dispatch— every allowlisted tool call is launched through the isolated executor lane the moment its call block (<function_call>,<tool_call>,<function=>) closes in the token stream. N calls per response run concurrently; each commits independently. Calls whose arguments reference earlier results via"$k.path"placeholders are resolved and executed server-side, wave by wave (a k-step chain costs one planning turn instead of k). Results return asdflash_early_dispatch(JSON) / inside thedflash_tool_speculationSSE event.--end-turn-snapshot— after a tool-enabled generation the server snapshots prompt+output into the inline prefix cache, so the next turn restores the whole conversation instead of a stale boundary.--prefetch-prefill— after a turn whose early-dispatched results all committed, the next request is fully determined (conversation + assistant output + canonical tool messages). The server renders it, prefills it and caches the KV before the client's next request arrives; clients that echo each hit'stool_message_contentstring start their next turn at decode speed, paying the turn's dominant cost during the network round-trip instead of on the critical path.Clients can still supply a concrete prediction via the
tool_speculationextension (gated by a measured--tool-spec-profile), and opt out per request with"automatic_tool_speculation": false.What was removed, and why
The earlier revisions carried a pre-generation Qwen3-0.6B predictor lane (native IPC daemon, semantic parser, HTTP sidecar) plus ~20k lines of replay benchmarks whose latencies were synthetic (2 s sleep per call). Measured on real tools the predictor hit 3/17 calls on live public APIs and 0/75 on terminal-bench, against ~0.5 s of serial cost per turn — so it was removed (history preserves it). The PR is now ~5k lines total including the harness and artifacts, of which ~2.5k are engine code.
Measured (Lucebox5, DeepSeek-V4-0731 + DSpark, live public APIs)
10 multi-step tasks over keyless real APIs (geocoding, weather, World Bank, Wikipedia, FX; 0.1-0.9 s latency), control vs speculative on the same server, answers identical (
optimizations/ooo_spec_lucebox5_cpu/realapi/):Many-call tasks (4-8 calls each): parallel calls + early dispatch finish in 11 model turns vs 32 for one-call-per-turn, 20/20 calls hidden, 4/4 vs 2/4 tasks correct, 1.28x end to end.
Prefetch-prefill (this revision, full stack,
realapi/results/rest_full_stack_10tasks.json): paired speedup x1.44 aggregate / x1.30 median over the same 10 tasks, 17/17 calls hidden, 21.4 s of client tool wait removed, and 72.3 s of prefill moved off the critical path across 15 prefetched turns ([prefetch] next-turn KV ready ... off the critical pathper turn); 9/10 tasks correct vs 8/10 for control, answers otherwise identical.Gains scale with tool latency and call count; on benchmarks with millisecond in-memory tools there is nothing to hide.
Safety
Unchanged from the hardened lane: allowlist-only, no shell, minimal environment, closed descriptors, optional pinned CPU lane disjoint from the model, process-group cleanup, bounded output; result deadline now counts from the authoritative commit (a slow generation can no longer expire a finished result).
Concurrency (production serving)
[prefetch] yielded to a queued request). A waiting client never pays for speculative prefill.--tool-spec-max-executors, default 16): attempts beyond it defer with reasonexecutor_saturated; slot accounting is exact across reap/terminate/destructor paths (unit-tested).Verification
realapi/results/Follow-ups
"tool_execution": "server"): consume early-dispatch results in-place and return the final answer in one request.